/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dx.dex.file;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;

import com.android.dx.rop.annotation.Annotations;
import com.android.dx.rop.annotation.AnnotationsList;
import com.android.dx.rop.cst.CstFieldRef;
import com.android.dx.rop.cst.CstMethodRef;
import com.android.dx.util.AnnotatedOutput;
import com.android.dx.util.Hex;

/**
 * Per-class directory of annotations.
 */
public final class AnnotationsDirectoryItem extends OffsettedItem {

	/** the required alignment for instances of this class */
	private static final int ALIGNMENT = 4;

	/** write size of this class's header, in bytes */
	private static final int HEADER_SIZE = 16;

	/** write size of a list element, in bytes */
	private static final int ELEMENT_SIZE = 8;

	/** {@code null-ok;} the class-level annotations, if any */
	private AnnotationSetItem classAnnotations;

	/** {@code null-ok;} the annotated fields, if any */
	private ArrayList<FieldAnnotationStruct> fieldAnnotations;

	/** {@code null-ok;} the annotated methods, if any */
	private ArrayList<MethodAnnotationStruct> methodAnnotations;

	/** {@code null-ok;} the annotated parameters, if any */
	private ArrayList<ParameterAnnotationStruct> parameterAnnotations;

	/**
	 * Constructs an empty instance.
	 */
	public AnnotationsDirectoryItem() {
		super(ALIGNMENT, -1);

		classAnnotations = null;
		fieldAnnotations = null;
		methodAnnotations = null;
		parameterAnnotations = null;
	}

	/** {@inheritDoc} */
	@Override
	public ItemType itemType() {
		return ItemType.TYPE_ANNOTATIONS_DIRECTORY_ITEM;
	}

	/**
	 * Returns whether this item is empty (has no contents).
	 * 
	 * @return {@code true} if this item is empty, or {@code false} if not
	 */
	public boolean isEmpty() {
		return (classAnnotations == null) && (fieldAnnotations == null)
				&& (methodAnnotations == null)
				&& (parameterAnnotations == null);
	}

	/**
	 * Returns whether this item is a candidate for interning. The only
	 * interning candidates are ones that <i>only</i> have a non-null set of
	 * class annotations, with no other lists.
	 * 
	 * @return {@code true} if this is an interning candidate, or {@code false}
	 *         if not
	 */
	public boolean isInternable() {
		return (classAnnotations != null) && (fieldAnnotations == null)
				&& (methodAnnotations == null)
				&& (parameterAnnotations == null);
	}

	/** {@inheritDoc} */
	@Override
	public int hashCode() {
		if (classAnnotations == null) {
			return 0;
		}

		return classAnnotations.hashCode();
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * <b>Note:</b>: This throws an exception if this item is not internable.
	 * </p>
	 * 
	 * @see #isInternable
	 */
	@Override
	public int compareTo0(OffsettedItem other) {
		if (!isInternable()) {
			throw new UnsupportedOperationException("uninternable instance");
		}

		AnnotationsDirectoryItem otherDirectory = (AnnotationsDirectoryItem) other;
		return classAnnotations.compareTo(otherDirectory.classAnnotations);
	}

	/**
	 * Sets the direct annotations on this instance. These are annotations made
	 * on the class, per se, as opposed to on one of its members. It is only
	 * valid to call this method at most once per instance.
	 * 
	 * @param annotations
	 *            {@code non-null;} annotations to set for this class
	 */
	public void setClassAnnotations(Annotations annotations) {
		if (annotations == null) {
			throw new NullPointerException("annotations == null");
		}

		if (classAnnotations != null) {
			throw new UnsupportedOperationException(
					"class annotations already set");
		}

		classAnnotations = new AnnotationSetItem(annotations);
	}

	/**
	 * Adds a field annotations item to this instance.
	 * 
	 * @param field
	 *            {@code non-null;} field in question
	 * @param annotations
	 *            {@code non-null;} associated annotations to add
	 */
	public void addFieldAnnotations(CstFieldRef field, Annotations annotations) {
		if (fieldAnnotations == null) {
			fieldAnnotations = new ArrayList<FieldAnnotationStruct>();
		}

		fieldAnnotations.add(new FieldAnnotationStruct(field,
				new AnnotationSetItem(annotations)));
	}

	/**
	 * Adds a method annotations item to this instance.
	 * 
	 * @param method
	 *            {@code non-null;} method in question
	 * @param annotations
	 *            {@code non-null;} associated annotations to add
	 */
	public void addMethodAnnotations(CstMethodRef method,
			Annotations annotations) {
		if (methodAnnotations == null) {
			methodAnnotations = new ArrayList<MethodAnnotationStruct>();
		}

		methodAnnotations.add(new MethodAnnotationStruct(method,
				new AnnotationSetItem(annotations)));
	}

	/**
	 * Adds a parameter annotations item to this instance.
	 * 
	 * @param method
	 *            {@code non-null;} method in question
	 * @param list
	 *            {@code non-null;} associated list of annotation sets to add
	 */
	public void addParameterAnnotations(CstMethodRef method,
			AnnotationsList list) {
		if (parameterAnnotations == null) {
			parameterAnnotations = new ArrayList<ParameterAnnotationStruct>();
		}

		parameterAnnotations.add(new ParameterAnnotationStruct(method, list));
	}

	/**
	 * Gets the method annotations for a given method, if any. This is meant for
	 * use by debugging / dumping code.
	 * 
	 * @param method
	 *            {@code non-null;} the method
	 * @return {@code null-ok;} the method annotations, if any
	 */
	public Annotations getMethodAnnotations(CstMethodRef method) {
		if (methodAnnotations == null) {
			return null;
		}

		for (MethodAnnotationStruct item : methodAnnotations) {
			if (item.getMethod().equals(method)) {
				return item.getAnnotations();
			}
		}

		return null;
	}

	/**
	 * Gets the parameter annotations for a given method, if any. This is meant
	 * for use by debugging / dumping code.
	 * 
	 * @param method
	 *            {@code non-null;} the method
	 * @return {@code null-ok;} the parameter annotations, if any
	 */
	public AnnotationsList getParameterAnnotations(CstMethodRef method) {
		if (parameterAnnotations == null) {
			return null;
		}

		for (ParameterAnnotationStruct item : parameterAnnotations) {
			if (item.getMethod().equals(method)) {
				return item.getAnnotationsList();
			}
		}

		return null;
	}

	/** {@inheritDoc} */
	public void addContents(DexFile file) {
		MixedItemSection wordData = file.getWordData();

		if (classAnnotations != null) {
			classAnnotations = wordData.intern(classAnnotations);
		}

		if (fieldAnnotations != null) {
			for (FieldAnnotationStruct item : fieldAnnotations) {
				item.addContents(file);
			}
		}

		if (methodAnnotations != null) {
			for (MethodAnnotationStruct item : methodAnnotations) {
				item.addContents(file);
			}
		}

		if (parameterAnnotations != null) {
			for (ParameterAnnotationStruct item : parameterAnnotations) {
				item.addContents(file);
			}
		}
	}

	/** {@inheritDoc} */
	@Override
	public String toHuman() {
		throw new RuntimeException("unsupported");
	}

	/** {@inheritDoc} */
	@Override
	protected void place0(Section addedTo, int offset) {
		// We just need to set the write size here.

		int elementCount = listSize(fieldAnnotations)
				+ listSize(methodAnnotations) + listSize(parameterAnnotations);
		setWriteSize(HEADER_SIZE + (elementCount * ELEMENT_SIZE));
	}

	/** {@inheritDoc} */
	@Override
	protected void writeTo0(DexFile file, AnnotatedOutput out) {
		boolean annotates = out.annotates();
		int classOff = OffsettedItem.getAbsoluteOffsetOr0(classAnnotations);
		int fieldsSize = listSize(fieldAnnotations);
		int methodsSize = listSize(methodAnnotations);
		int parametersSize = listSize(parameterAnnotations);

		if (annotates) {
			out.annotate(0, offsetString() + " annotations directory");
			out.annotate(4, "  class_annotations_off: " + Hex.u4(classOff));
			out.annotate(4, "  fields_size:           " + Hex.u4(fieldsSize));
			out.annotate(4, "  methods_size:          " + Hex.u4(methodsSize));
			out.annotate(4,
					"  parameters_size:       " + Hex.u4(parametersSize));
		}

		out.writeInt(classOff);
		out.writeInt(fieldsSize);
		out.writeInt(methodsSize);
		out.writeInt(parametersSize);

		if (fieldsSize != 0) {
			Collections.sort(fieldAnnotations);
			if (annotates) {
				out.annotate(0, "  fields:");
			}
			for (FieldAnnotationStruct item : fieldAnnotations) {
				item.writeTo(file, out);
			}
		}

		if (methodsSize != 0) {
			Collections.sort(methodAnnotations);
			if (annotates) {
				out.annotate(0, "  methods:");
			}
			for (MethodAnnotationStruct item : methodAnnotations) {
				item.writeTo(file, out);
			}
		}

		if (parametersSize != 0) {
			Collections.sort(parameterAnnotations);
			if (annotates) {
				out.annotate(0, "  parameters:");
			}
			for (ParameterAnnotationStruct item : parameterAnnotations) {
				item.writeTo(file, out);
			}
		}
	}

	/**
	 * Gets the list size of the given list, or {@code 0} if given {@code null}.
	 * 
	 * @param list
	 *            {@code null-ok;} the list in question
	 * @return {@code >= 0;} its size
	 */
	private static int listSize(ArrayList<?> list) {
		if (list == null) {
			return 0;
		}

		return list.size();
	}

	/**
	 * Prints out the contents of this instance, in a debugging-friendly way.
	 * This is meant to be called from {@link ClassDefItem#debugPrint}.
	 * 
	 * @param out
	 *            {@code non-null;} where to output to
	 */
	/* package */void debugPrint(PrintWriter out) {
		if (classAnnotations != null) {
			out.println("  class annotations: " + classAnnotations);
		}

		if (fieldAnnotations != null) {
			out.println("  field annotations:");
			for (FieldAnnotationStruct item : fieldAnnotations) {
				out.println("    " + item.toHuman());
			}
		}

		if (methodAnnotations != null) {
			out.println("  method annotations:");
			for (MethodAnnotationStruct item : methodAnnotations) {
				out.println("    " + item.toHuman());
			}
		}

		if (parameterAnnotations != null) {
			out.println("  parameter annotations:");
			for (ParameterAnnotationStruct item : parameterAnnotations) {
				out.println("    " + item.toHuman());
			}
		}
	}
}
